package com.navercorp.utilset.cipher;

import com.navercorp.utilset.utils.UtilSetLogUtils;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

/**
 * @author jaemin.woo
 *
 */
public class AesCipher implements CipherObject {
    private static final String TAG = AesCipher.class.getSimpleName();

    private static final String AES = "AES";

    private static final String HEX = "0123456789ABCDEF";

    private static final int KEY_SIZE = 128;

    private static final int RADIX = 16;

    private static final int NUMBER_TWO = 2;

    private static final int NUMBER_FOUR = 4;

    private static final int HEX_CHECKER = 0x0f;

    private static SecretKey secretKey;

    private static String sEncryptedText;

    private static String sDecryptedText;

    /**
     * getsEncryptedText
     *
     * @return sEncryptedText String
     */
    public static String getsEncryptedText() {
        return sEncryptedText;
    }

    /**
     * getsDecryptedText
     *
     * @return sDecryptedText String
     */
    public static String getsDecryptedText() {
        return sDecryptedText;
    }

    @Override
    public void encrypt(String seed, String plaintext) {
        if (seed.length() > 0 && plaintext.length() > 0) {
            byte[] rawKey = getRawKey(seed.getBytes());
            byte[] result = encrypt(rawKey, plaintext.getBytes());
            sEncryptedText = toHex(result);
        }
    }

    private byte[] encrypt(byte[] raw, byte[] clear) {
        SecretKeySpec secretKeySpec = new SecretKeySpec(raw, AES);
        Cipher cipher;
        byte[] encrypted = null;
        try {
            cipher = Cipher.getInstance(AES);
            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
            encrypted = cipher.doFinal(clear);
        } catch (NoSuchAlgorithmException e) {
            handleGettingAesFailure();
        } catch (NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException | InvalidKeyException e) {
            // Exceptions under here occurs because of programming error
            UtilSetLogUtils.error(TAG, "Failed to get SecureRandom Crypto Algorithm");
        }

        return encrypted;
    }

    @Override
    public void decrypt(String seed, String cipherText) {
        if (seed.length() > 0 && cipherText.length() > 0) {
            byte[] rawKey = getDecryptRawKey(seed.getBytes());
            byte[] enc = toByte(cipherText);
            byte[] result = decrypt(rawKey, enc);
            sDecryptedText = new String(result);
        }
    }

    private byte[] decrypt(byte[] raw, byte[] encrypted) {
        SecretKeySpec secretKeySpec = new SecretKeySpec(raw, AES);
        Cipher cipher;
        byte[] decrypted = null;
        try {
            cipher = Cipher.getInstance(AES);
            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
            decrypted = cipher.doFinal(encrypted);
        } catch (NoSuchAlgorithmException e) {
            // Can't be happened
            handleGettingAesFailure();
        } catch (NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
            // Exceptions under here occurs because of programming error
            UtilSetLogUtils.error(TAG, "Failed to get SecureRandom Crypto Algorithm");
        }

        return decrypted;
    }

    private static void handleGettingAesFailure() {
        UtilSetLogUtils.error(TAG, "Getting AES provider has failed but this can't be happened");
    }

    private static byte[] getRawKey(byte[] seed) {
        KeyGenerator kgen = null;

        try {
            kgen = KeyGenerator.getInstance(AES);
            SecureRandom sr = SecureRandom.getInstance("SHA1PRNG", "Crypto");
            if (sr == null) {
                return null;
            }
            sr.setSeed(seed);
            kgen.init(KEY_SIZE, sr); // 192 and 256 bits may not be available
        } catch (NoSuchAlgorithmException e) {
            // Impossible
            handleGettingAesFailure();
        } catch (NoSuchProviderException e) {
            UtilSetLogUtils.error(TAG, "NoSuchProviderException");
        }

        if (kgen != null) {
            secretKey = kgen.generateKey();
            return secretKey.getEncoded();
        } else {
            return null;
        }
    }

    private static byte[] getDecryptRawKey(byte[] seed) {
        KeyGenerator kgen = null;

        try {
            kgen = KeyGenerator.getInstance(AES);
            SecureRandom sr = SecureRandom.getInstance("SHA1PRNG", "Crypto");
            if (sr == null) {
                return null;
            }
            sr.setSeed(seed);
            kgen.init(KEY_SIZE, sr); // 192 and 256 bits may not be available
        } catch (NoSuchAlgorithmException e) {
            // Impossible
            handleGettingAesFailure();
        } catch (NoSuchProviderException e) {
            UtilSetLogUtils.error(TAG, "NoSuchProviderException");
        }

        if (kgen != null) {
            if (secretKey != null) {
                return secretKey.getEncoded();
            } else {
                secretKey = kgen.generateKey();
                return secretKey.getEncoded();
            }
        } else {
            return null;
        }
    }

    private byte[] toByte(String hexString) {
        int len = hexString.length() / NUMBER_TWO;
        byte[] result = new byte[len];
        for (int index = 0; index < len; index++) {
            String value = hexString.substring(NUMBER_TWO * index, NUMBER_TWO * index + NUMBER_TWO);
            result[index] = Integer.valueOf(value, RADIX).byteValue();
        }
        return result;
    }

    private String toHex(byte[] buf) {
        if (buf == null) {
            return "";
        }
        StringBuffer result = new StringBuffer(NUMBER_TWO * buf.length);
        for (byte byteValue : buf) {
            appendHex(result, byteValue);
        }
        return result.toString();
    }

    private static void appendHex(StringBuffer sb, byte bt) {
        sb.append(HEX.charAt((bt >> NUMBER_FOUR) & HEX_CHECKER)).append(HEX.charAt(bt & HEX_CHECKER));
    }
}
